今天學習seTransition 與 useDeferredValue。這兩個 Hook 能讓我們以「聲明式」的方式,將特定 UI 更新標記為「非緊急任務(Non-urgent Updates)」,完美解決打字輸入框 lag、巨量列表渲染卡頓等效能痛點!
在過去,面對「輸入框搜尋帶動下方巨量列表重繪」的卡頓問題,開發者常用的手法是 lodash.debounce 或 setTimeout:
// 傳統 Debounce:強制延遲 300ms 執行
const handleSearch = debounce((query) => {
setSearchQuery(query);
}, 300);
傳統 Debounce 的兩大局限:
人為的人工延遲:不管使用者的電腦效能多強,都要死板地等待 300ms 才能看到結果,缺乏靈活性。
一旦執行依然卡頓:300ms 時間一到,列表渲染依然會同步且不可中斷地佔用主執行緒,造成畫面瞬間凍結。
當你有自己可以控制的狀態變更函數 (setState),且希望它不要阻塞 UI 輸入時,使用 useTransition。
語法範例:
useTransition 回傳一個包含兩個元素的陣列:
isPending:布林值,代表背景的低優先級渲染是否正在進行中(可用於顯示 Spinner)。
startTransition:函數,將傳入的狀態更新封裝為非緊急任務。
import { useState, useTransition } from 'react';
function SearchPage() {
const [inputVal, setInputVal] = useState('');
const [listQuery, setListQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// 1. 緊急任務:立即更新 Input,確保打字 100% 流暢
setInputVal(e.target.value);
// 2. 非緊急任務:包裹在 startTransition 中,允許被打字中斷
startTransition(() => {
setListQuery(e.target.value);
});
};
return (
<div>
<input value={inputVal} onChange={handleChange} />
{isPending && <p>載入列表中...</p>}
<HeavyList query={listQuery} />
</div>
);
}
如果你無法直接存取 setState(例如值是從父組件傳入的 Props,或是第三方套件提供的狀態),useDeferredValue 就能解決。
語法範例:
useDeferredValue 接受一個數值,並回傳一個「延遲更新」的新數值。
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
// 將 query 轉換為低優先級的延遲值
const deferredQuery = useDeferredValue(query);
return (
<div>
{/* 1. Input 綁定原始 query,保持即時響應 */}
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{/* 2. HeavyList 傳入 deferredQuery,React 會優先渲染 Input,*/}
{/* 並在背景利用空閒時間渲染 HeavyList */}
<HeavyList query={deferredQuery} />
</div>
);
}
